package edu.pku.sei.modeler.jet.codegen;


import java.io.ByteArrayInputStream;
import java.io.InputStream;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.emf.codegen.CodeGen;
import org.eclipse.emf.codegen.jet.JETEmitter;
import org.eclipse.emf.codegen.jet.JETException;
import org.eclipse.emf.codegen.jmerge.JControlModel;
import org.eclipse.emf.codegen.jmerge.JMerger;


/**
 * @author jesse
 *
 */
public class JETGateway
{

  private Config mConfig = null;

  public JETGateway(Config config)
  {
    mConfig = config;
  }

  /**
   * Invokes the JET template specified in the <code>Config</code> with the
   * model specified in the <code>Config</code> and returns the generated text
   * as a String.
   * <p>
   * This implementation uses a <code>JETEmitter</code> to translate the
   * template to a Java implementation class. The translated class is created in
   * a hidden project called <code>.JETEmitters</code>.
   * <p>
   * In order to be able to compile the translated template implementation
   * class, the classes used by the model specified in the <code>Config</code>
   * must be available in the classpath. For this reason, this method sets the
   * first runtime library of the plugin specified in the <code>Config</code>
   * as a classpath variable to the <code>.JETEmitters</code> project.
   * 
   * @param monitor
   *          the progress monitor to use. May be <code>null</code>.
   * @return the source code text generated by the JET template
   * @throws CoreException
   */
  public String generate(IProgressMonitor monitor) throws CoreException
  {
    monitor = createIfNull(monitor);

    Config config = getConfig();
    JETEmitter emitter = new JETEmitter(config.getTemplateFullUri(), getClass().getClassLoader());
    try {
		emitter.addVariable(config.getClasspathVariable(), config.getPluginId());
	} catch (JETException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}

    IProgressMonitor sub = new SubProgressMonitor(monitor, 1);
    String result="";
	try {
		result = emitter.generate(sub, new Object []{ config.getModel() });
	} catch (JETException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
    monitor.worked(1);
    return result;
  }

  /**
   * Merges the specified emitterResult with the contents of an existing file
   * and returns the result. The existing file is not modified.
   * <p>
   * The location of the file to merge with is found by finding or creating the
   * container (folder) for the <code>Config</code>'s package in the
   * <code>Config</code>'s target folder. The name of the file to merge with
   * is the <code>Config</code>'s target file.
   * 
   * @param monitor
   *          the progress monitor to use. May be <code>null</code>.
   * @param emitterResult
   *          generated content to merge with the existing content
   * @return the result of merging the specified generated contents with the
   *         existing file
   * @throws CoreException
   *           if an error occurs accessing the contents of the file
   */
  public String merge(IProgressMonitor monitor, String emitterResult) throws CoreException
  {
    monitor = createIfNull(monitor);

    Config config = getConfig();
    IContainer container = findOrCreateContainer(monitor, config.getTargetFolder(), config.getPackageName());
    if (container == null)
    {
      try {
		throw new JETException("Cound not find or create container for package " + config.getPackageName() + " in "
		    + config.getTargetFolder());
	} catch (JETException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
    }

    IFile targetFile = container.getFile(new Path(config.getTargetFile()));
    if (!targetFile.exists())
    {
      monitor.worked(1);
      return emitterResult;
    }

    JMerger jMerger = new JMerger();
    jMerger.setControlModel(new JControlModel(config.getMergeXmlFullUri()));
    jMerger.setSourceCompilationUnit(jMerger.createCompilationUnitForContents(emitterResult));

    jMerger.setTargetCompilationUnit(jMerger.createCompilationUnitForInputStream(targetFile.getContents(true)));
    String oldContents = jMerger.getTargetCompilationUnit().getContents();

    jMerger.merge();
    monitor.worked(1);

    String result = jMerger.getTargetCompilationUnit().getContents();
    if (oldContents.equals(result))
    {
      return result;
    }

    if (!targetFile.isReadOnly())
    {
      return result;
    }

    // The file may be read-only because it is checked out
    // by a VCM component. Here we ask permission to change the file.
    if (targetFile.getWorkspace().validateEdit(new IFile []{ targetFile }, new SubProgressMonitor(monitor, 1)).isOK())
    {

      jMerger.setTargetCompilationUnit(jMerger.createCompilationUnitForInputStream(targetFile.getContents(true)));
      jMerger.remerge();
      return jMerger.getTargetCompilationUnit().getContents();
    }
    return result;
  }

  /**
   * Saves the specified contents to a location specified by the
   * <code>Config</code> settings. The location of the file to save is found
   * by finding or creating the container (folder) for the <code>Config</code>
   * 's package in the <code>Config</code>'s target folder. The name of the
   * file to save is the <code>Config</code>'s target file.
   * 
   * @param monitor
   *          the progress monitor to use. May be <code>null</code>.
   * @param contents
   *          the byte contents of the file to save
   * @throws CoreException
   */
  public IFile save(IProgressMonitor monitor, byte[] contents) throws CoreException
  {
    monitor = createIfNull(monitor);

    Config config = getConfig();
    IContainer container = findOrCreateContainer(monitor, config.getTargetFolder(), config.getPackageName());
    if (container == null)
    {
      try {
		throw new JETException("Cound not find or create container for package " + config.getPackageName() + " in "
		    + config.getTargetFolder());
	} catch (JETException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
    }
    IFile targetFile = container.getFile(new Path(config.getTargetFile()));
    IFile result = getWritableTargetFile(targetFile, container, config.getTargetFile());

    InputStream newContents = new ByteArrayInputStream(contents);
    if (result.exists())
    {
      result.setContents(newContents, true, true, new SubProgressMonitor(monitor, 1));
    }
    else
    {
      result.create(newContents, true, new SubProgressMonitor(monitor, 1));
    }
    return result;
  }

  public IFile save(IProgressMonitor monitor, InputStream in) throws CoreException
  {
    monitor = createIfNull(monitor);

    Config config = getConfig();
    IContainer container = findOrCreateContainer(monitor, config.getTargetFolder(), config.getPackageName());
    if (container == null)
    {
      try {
		throw new JETException("Cound not find or create container for package " + config.getPackageName() + " in "
		    + config.getTargetFolder());
	} catch (JETException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
    }
    IFile targetFile = container.getFile(new Path(config.getTargetFile()));
    IFile result = getWritableTargetFile(targetFile, container, config.getTargetFile());

    if (result.exists())
    {
      result.setContents(in, true, true, new SubProgressMonitor(monitor, 1));
    }
    else
    {
      result.create(in, true, new SubProgressMonitor(monitor, 1));
    }
    return result;
  }

  /**
   * Returns a non-null progress monitor.
   * 
   * @param monitor
   *          an existing progress monitor
   * @return a new <code>NullProgressMonitor</code> if the specified monitor
   *         is <code>null</code>, or the existing monitor otherwise
   */
  private IProgressMonitor createIfNull(IProgressMonitor monitor)
  {
    if (monitor == null)
    {
      return new NullProgressMonitor();
    }
    return monitor;
  }

  private IContainer findOrCreateContainer(IProgressMonitor progressMonitor, String targetDirectory, String packageName) throws CoreException
  {

    IPath outputPath = new Path(targetDirectory + "/" + packageName.replace('.', '/'));
    progressMonitor.beginTask("", 4);

    IProgressMonitor sub = new SubProgressMonitor(progressMonitor, 1);
    IPath localLocation = null; // use default
    IContainer container = CodeGen.findOrCreateContainer(outputPath, true, localLocation, sub);
    return container;
  }

  /**
   * Returns a <code>IFile</code> that can be written to. If the specified
   * file is read-write, it is returned unchanged. If the specified file is
   * read-only and {@link Config#isForceOverwrite()}returns <code>true</code>,
   * the file is made writable, otherwise a new file is returned in the
   * specified container with filename <code>"." + fileName + ".new"</code>.
   * 
   * @param container
   *          container to create the new file in if the specified file cannot
   *          be made writable
   * @param targetFile
   *          the file to make writable
   * @param fileName
   *          used to create a new file name if the specified file cannot be
   *          made writable
   * @return a <code>IFile</code> that can be written to
   */
  private IFile getWritableTargetFile(IFile targetFile, IContainer container, String fileName)
  {
    if (targetFile.isReadOnly())
    {
      if (getConfig().isForceOverwrite())
      {
        targetFile.setReadOnly(false);
      }
      else
      {
        targetFile = container.getFile(new Path("." + fileName + ".new"));
      }
    }
    return targetFile;
  }

  /**
   * Returns the <code>Config</code> object
   * 
   * @return the <code>Config</code> object
   */
  protected Config getConfig()
  {
    return mConfig;
  }
}